/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
#include "cmdutils.h"
#include "ffmpeg.h"
#include "../ffmpeg_thread/ffmpeg_thread.h"
#include "../ffmpeg_thread/ffprobe_thread.h"
#include "js_native_api.h"
#include "libavutil/pixfmt.h"
#include "../log/ohos_log.h"
#include "node_api.h"
#include "native_common.h"
#include <filesystem>
#include <pthread.h>
#include <stdbool.h>
#include <string>
#include <unistd.h>
#include <uv.h>
#include <climits>

#ifdef __cplusplus
#define MIN_TIME_DIFF 0.17
#define POST_MSG_WAIT_TIME 1
extern "C" {
#include "custom_ffmpeg_log.h"
#include "ffmpeg_mediametadataretriever.h"
#endif

int FFMPEG_HANDLE_RESULT = -1;
int g_ffprobeHandleResult = INT_MAX;
std::string g_ffprobeHandleResultStr = "";
int FFMPEG_CMD_NUM = 0;
char **exeArgvCmd;
State *state = NULL;
std::mutex g_stateMutex;
const int DEFAULT_ZERO = 0;
const int SLEEP_TIME = 10000;
const int32_t INDEX_0 = 0;
const int32_t INDEX_1 = 1;
const int32_t INDEX_2 = 2;
const int32_t INDEX_3 = 3;
const int32_t INDEX_4 = 4;
const int32_t INDEX_5 = 5;
const int32_t PARAM_COUNT_1 = 1;
const int32_t PARAM_COUNT_2 = 2;
const int32_t PARAM_COUNT_3 = 3;
const int32_t PARAM_COUNT_4 = 4;
const int32_t PARAM_COUNT_5 = 5;
const int32_t TIME_MULNUMBER = 1000000;
bool STOP_GET_FRAME = false;

struct AsyncCallbackInfo 
{
    napi_env env;
    napi_async_work asyncWork;
    napi_deferred deferred;
    napi_ref callbackRef;
    int code;
};

struct GetFrameAsyncCallbackInfo 
{
    napi_env env;
    napi_async_work asyncWork;
    napi_deferred deferred;
    std::string path;
    std::string timeUs;
    std::string timeUsStart;
    std::string timeUsEnd;
    std::string option;
    AVPacket aVPacketFrame;
    napi_ref callbackRef;
    napi_ref messageCallbackRef;
};

class FrameOption final
{
public:
    static const char * const PARAM_PATH;
    static const char * const PARAM_STIMEUS;
    static const char * const PARAM_ETIMEUS;
    static const char * const PARAM_OPTION;
};

const char * const FrameOption::PARAM_PATH = "path";
const char * const FrameOption::PARAM_STIMEUS = "stimeUs";
const char * const FrameOption::PARAM_ETIMEUS = "etimeUs";
const char * const FrameOption::PARAM_OPTION = "option";

void ffmpeg_callback(int ret) 
{
    LOGD("ffmpeg_callback enter");
    FFMPEG_HANDLE_RESULT = ret;
    LOGD("ffmpeg_callback leave");
}

void FfprobeCallback(char* result, int length)
{
    LOGD("FfprobeCallback enter");
    g_ffprobeHandleResult = length;
    g_ffprobeHandleResultStr = "";
    if (result != nullptr && length != 0) {
        g_ffprobeHandleResultStr = std::string(result, length);
    }
    LOGD("FfprobeCallback enter");
}

char *getStringFromJS(napi_env env, napi_value value)
{
    LOGD("getStringFromJS enter");
    size_t strLength = 0;
    napi_get_value_string_utf8(env, value, NULL, 0, &strLength);

    if (strLength == 0) {
        LOGW("getStringFromJS: strLength is null");
        LOGD("getStringFromJS leave");
        return nullptr;
    }
    
    char *str = static_cast<char *>(malloc(strLength + 1));
    if (str == nullptr) {
        LOGE("getStringFromJS: memory allocation failed");
        LOGD("getStringFromJS leave");
        return nullptr;
    }

    napi_get_value_string_utf8(env, value, str, strLength + 1, &strLength);
    str[strLength] = '\0';
    LOGD("getStringFromJS leave");
    return str;
}

char **getArrayFromSpecialString(char *value) 
{
    LOGD("getArrayFromSpecialString enter");
    std::vector<std::string> tokens;
    const std::string str = value;
    std::string token;
    bool inQuotes = false;

    for (size_t i = 0; i < str.size(); ++i) {
        if (str[i] == '"') {
            inQuotes = !inQuotes;
            continue;
        }

        if (str[i] == ' ' && !inQuotes) {
            if (!token.empty()) {
                tokens.push_back(token);
                token.clear();
            }
        } else {
            token += str[i];
        }
    }

    if (!token.empty()) {
        tokens.push_back(token);
    }

    char **result = new char *[tokens.size() + 1]; // +1 for NULL terminator

    for (size_t i = 0; i < tokens.size(); ++i) {
        result[i] = new char[tokens[i].size() + 1];
        std::strcpy(result[i], tokens[i].c_str());
    }

    FFMPEG_CMD_NUM = tokens.size();

    result[tokens.size()] = nullptr; // NULL terminate the array
    LOGD("getArrayFromSpecialString leave");
    return result;
}

std::string jsValueToString(const napi_env &env, const napi_value &value) 
{
    LOGD("jsValueToString enter");
    size_t requiredLength = 0;
    napi_get_value_string_utf8(env, value, nullptr, 0, &requiredLength);

    if (requiredLength == 0) {
        LOGD("jsValueToString leave");
        return std::string();
    }

    std::string str(requiredLength, '\0');
    napi_get_value_string_utf8(env, value, &str[0], requiredLength + 1, nullptr);
    
    LOGD("jsValueToString leave");
    return str;
}

std::string GetStringProperty(napi_env env, napi_value object, const std::string &propertyName)
{
    bool hasProperty = false;
    napi_has_named_property(env, object, propertyName.c_str(), &hasProperty);
    if(!hasProperty) {
        return std::string();
    }
    napi_value value = nullptr;
    napi_get_named_property(env, object, propertyName.c_str(), &value);
    return jsValueToString(env, value);
}

static napi_value exeFFmpegCmd(napi_env env, napi_callback_info info) 
{
    LOGD("exeFFmpegCmd enter");
    FFMPEG_HANDLE_RESULT = -1;
    FFMPEG_CMD_NUM = 0;
    size_t argc = 2;
    napi_value argv[2];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    char *cmdTemp = getStringFromJS(env, argv[0]);
    exeArgvCmd = getArrayFromSpecialString(cmdTemp);
    free(cmdTemp);
    napi_deferred deferred;
    napi_value promise;
    NAPI_CALL(env, napi_create_promise(env, &deferred, &promise));
    AsyncCallbackInfo *asyncCallbackInfo = new AsyncCallbackInfo{
        .env = env,
        .asyncWork = nullptr,
        .deferred = deferred,
    };
    napi_value resourceName;
    napi_create_string_latin1(env, "ffmpegCMD", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName,
        [](napi_env env, void *data) {
            ffmpeg_thread_callback(ffmpeg_callback);
            ffmpeg_thread_run_cmd(FFMPEG_CMD_NUM, exeArgvCmd);
            while (true) {
                if (FFMPEG_HANDLE_RESULT != -1) {
                    break;
                }
                usleep(SLEEP_TIME);
            }
        },
        [](napi_env env, napi_status status, void *data) {
            AsyncCallbackInfo *asyncCallbackInfo = (AsyncCallbackInfo *)data;
            napi_value result;
            napi_create_int32(env, FFMPEG_HANDLE_RESULT, &result);
            napi_resolve_deferred(asyncCallbackInfo->env, asyncCallbackInfo->deferred, result);
            napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
            delete asyncCallbackInfo;
        },
        (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    LOGD("exeFFmpegCmd leave");
    return promise;
}

static napi_value exeFFprobeCmd(napi_env env, napi_callback_info info)
{
    LOGD("exeFFprobeCmd enter");
    g_ffprobeHandleResult = INT_MAX;
    FFMPEG_CMD_NUM = 0;
    size_t argc = 2;
    napi_value argv[2];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    char *cmdTemp = getStringFromJS(env, argv[0]);
    exeArgvCmd = getArrayFromSpecialString(cmdTemp);
    free(cmdTemp);
    napi_deferred deferred;
    napi_value promise;
    NAPI_CALL(env, napi_create_promise(env, &deferred, &promise));
    AsyncCallbackInfo *asyncCallbackInfo = new AsyncCallbackInfo{
        .env = env,
        .asyncWork = nullptr,
        .deferred = deferred,
    };
    napi_value resourceName;
    napi_create_string_latin1(env, "ffprobeCMD", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName,
        [](napi_env env, void *data) {
            FfprobeThreadCallback(FfprobeCallback);
            FfprobeThreadRunCmd(FFMPEG_CMD_NUM, exeArgvCmd);
            while (true) {
                if (g_ffprobeHandleResult != INT_MAX) {
                    break;
                }
                usleep(SLEEP_TIME);
            }
        },
        [](napi_env env, napi_status status, void *data) {
            AsyncCallbackInfo *asyncCallbackInfo = (AsyncCallbackInfo *)data;
            napi_value result;
            napi_create_string_utf8(env, g_ffprobeHandleResultStr.c_str(), g_ffprobeHandleResultStr.length(),
                                    &result);
            napi_resolve_deferred(asyncCallbackInfo->env, asyncCallbackInfo->deferred, result);
            napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
            delete asyncCallbackInfo;
        },
        (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    LOGD("exeFFprobeCmd leave");
    return promise;
}

static napi_value openNativeLog(napi_env env, napi_callback_info info) 
{
    LOGD("openNativeLog enter");
    open_custom_ffmpeg_log_print();
    LOGD("openNativeLog leave");
    return nullptr;
}

static napi_value setDataSource(napi_env env, napi_callback_info info) 
{
    LOGD("setDataSource enter");
    STOP_GET_FRAME = false;
    size_t argc = 2;
    napi_value argv[PARAM_COUNT_2];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    napi_deferred deferred;
    napi_value promise;
    NAPI_CALL(env, napi_create_promise(env, &deferred, &promise));
    std::string path = jsValueToString(env, argv[0]);
    GetFrameAsyncCallbackInfo *asyncCallbackInfo = new GetFrameAsyncCallbackInfo{
        .env = env,
        .asyncWork = nullptr,
        .deferred = deferred,
        .path = path,
    };
    napi_value resourceName;
    napi_create_string_latin1(env, "setDataSource", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName,
        [](napi_env env, void *data) {
            GetFrameAsyncCallbackInfo *asyncCallbackInfo = (GetFrameAsyncCallbackInfo *)data;
            set_data_source(&state, (char *)asyncCallbackInfo->path.c_str());
        },
        [](napi_env env, napi_status status, void *data) {
            GetFrameAsyncCallbackInfo *asyncCallbackInfo = (GetFrameAsyncCallbackInfo *)data;
            napi_value result;
            napi_create_int32(env, 0, &result);
            napi_resolve_deferred(asyncCallbackInfo->env, asyncCallbackInfo->deferred, result);
            napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
            delete asyncCallbackInfo;
        },
        (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    LOGD("setDataSource leave");
    return promise;
}

bool createArrayBuffer(napi_env env, void *src, size_t srcLen, napi_value *res) 
{
    LOGD("createArrayBuffer enter");
    if (src == nullptr || srcLen == DEFAULT_ZERO) {
        LOGW("src or srcLen is null");
        LOGD("createArrayBuffer leave");
        return false;
    }
    void *nativePtr = nullptr;
    if (napi_create_arraybuffer(env, srcLen, &nativePtr, res) != napi_ok || nativePtr == nullptr) {
        LOGE("napi_create_arraybuffer failed");
        LOGD("createArrayBuffer leave");
        return false;
    }
    memcpy(nativePtr, src, srcLen);
    LOGD("createArrayBuffer leave");
    return true;
}

void postMessage(void *data) 
{
    LOGD("postMessage enter");
    GetFrameAsyncCallbackInfo *asyncCallbackInfo = static_cast<GetFrameAsyncCallbackInfo*>(data);
    uv_loop_s *loopMessage = nullptr;
    napi_get_uv_event_loop(asyncCallbackInfo->env, &loopMessage);
    if (loopMessage == nullptr) {
        LOGE("napi_get_uv_event_loop failed");
        LOGD("postMessage leave");
        return;
    }
    uv_work_t *work = new uv_work_t;
    if (work == nullptr) {
        LOGE("memory allocation failed");
        LOGD("postMessage leave");
        return;
    }
    work->data = (void *)asyncCallbackInfo;
    uv_queue_work(
        loopMessage, work, [](uv_work_t *work) {},
        [](uv_work_t *work, int status) {
            if(STOP_GET_FRAME) return ;
            GetFrameAsyncCallbackInfo *context = (GetFrameAsyncCallbackInfo *)(work->data);
            napi_handle_scope scope = nullptr;
            napi_open_handle_scope(context->env, &scope);
            if (scope == nullptr) {
                LOGE("napi_open_handle_scope failed");
                return;
            }
            napi_value callback = nullptr;
            napi_get_reference_value(context->env, context->messageCallbackRef, &callback);
            napi_value result[PARAM_COUNT_2] = {nullptr};
            createArrayBuffer(context->env, context->aVPacketFrame.data, context->aVPacketFrame.size, &result[0]);
            napi_create_int32(context->env, atoi(context->timeUs.c_str()), &result[1]);
            napi_value returnVal;
            napi_call_function(context->env, nullptr, callback, PARAM_COUNT_2, result, &returnVal);
            napi_delete_reference(context->env, context->callbackRef);
            napi_close_handle_scope(context->env, scope);
            if (work != nullptr) {
                delete work;
            }
            context = NULL;
        });
    LOGD("postMessage leave");
}

static napi_value getFrameAtTimeRang(napi_env env, napi_callback_info info) {
    LOGD("getFrameAtTimeRang enter");
    size_t argc = 5;
    napi_value argv[PARAM_COUNT_5];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    auto asyncContext = new GetFrameAsyncCallbackInfo();
    asyncContext->env = env;
    napi_create_reference(env, argv[INDEX_3], 1, &asyncContext->messageCallbackRef);
    napi_value result;
    napi_get_undefined(env, &result);
    std::string timeUsStart = jsValueToString(env, argv[0]);
    std::string timeUsEnd = jsValueToString(env, argv[1]);
    std::string option = jsValueToString(env, argv[2]);
    GetFrameAsyncCallbackInfo *asyncCallbackInfo = new GetFrameAsyncCallbackInfo{
        .env = env,
        .asyncWork = nullptr,
        .messageCallbackRef = asyncContext->messageCallbackRef,
        .timeUsStart = timeUsStart,
        .timeUsEnd = timeUsEnd,
        .option = option,
    };
    napi_value resourceName;
    napi_create_string_latin1(env, "getFrameAtTimeRang", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName,
        [](napi_env env, void *data) {
            GetFrameAsyncCallbackInfo *asyncCallbackInfo = (GetFrameAsyncCallbackInfo *)data;
            int stime = atoi((char *)asyncCallbackInfo->timeUsStart.c_str());
            int etime = atoi((char *)asyncCallbackInfo->timeUsEnd.c_str());
            for (int i = stime; i <= etime; i = i + TIME_MULNUMBER) {
                if(STOP_GET_FRAME){
                  break;
                } 
                std::string tempTimeUs = std::to_string(i);
                auto start = std::chrono::high_resolution_clock::now();
                AVPacket avPacket = get_packet(&state, (char *)tempTimeUs.c_str(),
                                               (char *)asyncCallbackInfo->option.c_str());
                auto end = std::chrono::high_resolution_clock::now();
                std::chrono::duration<double> duration = end - start;
                double timeDiff = duration.count();
                asyncCallbackInfo->aVPacketFrame = avPacket;
                asyncCallbackInfo->timeUs = tempTimeUs;
                if ((timeDiff <= MIN_TIME_DIFF) && (i != 0)) {
                    sleep(POST_MSG_WAIT_TIME);
                }
                postMessage(asyncCallbackInfo);
            }
        },
        [](napi_env env, napi_status status, void *data) {
           releaseFrame(&state);
           state = NULL;
           GetFrameAsyncCallbackInfo *asyncCallbackInfo = (GetFrameAsyncCallbackInfo *)data;
           napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
        },
        (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    LOGD("getFrameAtTimeRang leave");
    return result;
}

static napi_value getFrame(napi_env env, napi_callback_info info) {
    STOP_GET_FRAME = false;
    size_t argc = 3;
    napi_value argv[PARAM_COUNT_3];
    napi_get_cb_info(env, info, &argc, argv, NULL, NULL);
    auto asyncContext = new GetFrameAsyncCallbackInfo();
    asyncContext->env = env;
    napi_create_reference(env, argv[INDEX_1], 1, &asyncContext->messageCallbackRef);
    napi_value result;
    napi_get_undefined(env, &result);
    std::string path = GetStringProperty(env, argv[0], FrameOption::PARAM_PATH);
    std::string timeUsStart = GetStringProperty(env, argv[0], FrameOption::PARAM_STIMEUS);
    std::string timeUsEnd = GetStringProperty(env, argv[0], FrameOption::PARAM_ETIMEUS);
    std::string option = GetStringProperty(env, argv[0], FrameOption::PARAM_OPTION);
    GetFrameAsyncCallbackInfo *asyncCallbackInfo = new GetFrameAsyncCallbackInfo{
        .env = env,
        .asyncWork = nullptr,
        .messageCallbackRef = asyncContext->messageCallbackRef,
        .path = path,
        .timeUsStart = timeUsStart,
        .timeUsEnd = timeUsEnd,
        .option = option,
    };
    napi_value resourceName;
    napi_create_string_latin1(env, "getFrame", NAPI_AUTO_LENGTH, &resourceName);
    napi_create_async_work(
        env, nullptr, resourceName,
        [](napi_env env, void *data) {
            GetFrameAsyncCallbackInfo *asyncCallbackInfo = static_cast<GetFrameAsyncCallbackInfo *>(data);
            int stime = atoi((char *)asyncCallbackInfo->timeUsStart.c_str());
            int etime = atoi((char *)asyncCallbackInfo->timeUsEnd.c_str());
            for (int i = stime; i <= etime; i = i + TIME_MULNUMBER) {
                if(STOP_GET_FRAME){
                  break;
                } 
                std::string tempTimeUs = std::to_string(i);
                auto start = std::chrono::high_resolution_clock::now();
                AVPacket avPacket;
                {
                    std::lock_guard<std::mutex> lock(g_stateMutex);
                    set_data_source(&state, (char *)asyncCallbackInfo->path.c_str());
                    avPacket = get_packet(&state, (char *)tempTimeUs.c_str(),
                                               (char *)asyncCallbackInfo->option.c_str());
                    releaseFrame(&state);
                    state = NULL;
                }
                auto end = std::chrono::high_resolution_clock::now();
                std::chrono::duration<double> duration = end - start;
                double timeDiff = duration.count();
                asyncCallbackInfo->aVPacketFrame = avPacket;
                asyncCallbackInfo->timeUs = tempTimeUs;
                if ((timeDiff <= MIN_TIME_DIFF) && (i != 0)) {
                    sleep(POST_MSG_WAIT_TIME);
                }
                postMessage(asyncCallbackInfo);
            }
        },
        [](napi_env env, napi_status status, void *data) {
           GetFrameAsyncCallbackInfo *asyncCallbackInfo = static_cast<GetFrameAsyncCallbackInfo *>(data);
           napi_delete_async_work(env, asyncCallbackInfo->asyncWork);
        },
        (void *)asyncCallbackInfo, &asyncCallbackInfo->asyncWork);
    napi_queue_async_work(env, asyncCallbackInfo->asyncWork);
    return result;
}


static napi_value stopGetFrame(napi_env env, napi_callback_info info) 
{
    LOGD("stopGetFrame enter");
    STOP_GET_FRAME = true;
    LOGD("stopGetFrame leave");
    return nullptr;
}

EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
    napi_property_descriptor desc[] = {
        DECLARE_NAPI_FUNCTION("exeFFmpegCmd", exeFFmpegCmd),
        DECLARE_NAPI_FUNCTION("exeFFprobeCmd", exeFFprobeCmd),
        DECLARE_NAPI_FUNCTION("openNativeLog", openNativeLog),
        DECLARE_NAPI_FUNCTION("setDataSource", setDataSource),
        DECLARE_NAPI_FUNCTION("getFrameAtTimeRang", getFrameAtTimeRang),
        DECLARE_NAPI_FUNCTION("getFrame", getFrame),
        DECLARE_NAPI_FUNCTION("stopGetFrame", stopGetFrame),
    };
    NAPI_CALL(env, napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc));
    return exports;
}
EXTERN_C_END

static napi_module mp4parserModule = {
    .nm_version = 1,
    .nm_flags = 0,
    .nm_filename = nullptr,
    .nm_register_func = Init,
    .nm_modname = "mp4parser_napi",
    .nm_priv = ((void *)0),
    .reserved = {0},
};

extern "C" __attribute__((constructor)) void RegisterModule(void) {
    napi_module_register(&mp4parserModule);
}

#ifdef __cplusplus
}
#endif